R

[R语言] 分组计算

这是一篇关于R语言快速分组计算的学习笔记

Posted by Leung ZhengHua on 2018-01-26

本文总点击量

或许在这之前你想使用并行计算:R语言并行化基础与提高

在大猫师兄的强烈推荐下,我研究了利用data.table包做分组计算。

以往做分组计算的思路是这样子的:先按照某个变量(比如股票stock)将数据集划分(split)为一个列表,内含若干个小数据框,然后自定义一个可以处理每个小数据框的函数。最后就可以利用lapply函数对列表中每个数据框做遍历了,这样子的速度很慢,可以用parallel包做个加速。为此我还写了个模板,记录下来,以后备用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
## 对含有大量数据框的列表做函数处理,每个数据框返回一个数据框
## 最后要合并所有数据框为一个数据框
### 函数名称
FUNC=out
### 含有大量数据框的列表
temp=temp
### 要导入各节点的函数和包,在下面添加
library(parallel)
library(rlist)
system.time({ # 用时约4分钟
# 注册节点,一个核可以算作一个节点
cl=makeCluster(detectCores())
# clusterExport 从某个环境调用某个对象给各个节点
clusterExport(cl, 'FUNC', envir = .GlobalEnv)
# 注册的每个节点相当于新开一个R session,clusterEvalQ表示在每个节点上执行相同的代码
# 这里是在每个节点(R session)里面导入滚动回归里面必须依赖的包
clusterEvalQ(cl,{
#setwd("I://anqi//项目//友情出演//xiang") # 数据存放路径
#library("dplyr")
#library("lubridate")
#library("tidyr")
#library("lazyeval")
library(fPortfolio)
})
# parLapply是Lapply的并行版本,parLapplyLB是负载平衡的版本,可以平衡各个节点的运行时间
# FUNC后面可以添加函数参数
re=parLapplyLB(cl,temp,FUNC)
# 使用时候销毁节点
stopCluster(cl)
})
result=list.rbind(re) # rlist里面这个rbind函数合并数据框的时候,没有把结果强制转换成矩阵,而是保留了原有的数据框格式,速度也很快

然而,开4核跑并行,做李翔的滚动回归时,要3分钟才可以出来结果,根据大猫师兄的说法,用data.table,都是1秒的事。在我算所有上市公司的市盈率和账面市值比BM等指标的时候,因为要把月频的财报折算为日频数据,并不是1秒钟可以解决的事情。当然可以继续优化下去,但data.table做分组计算确实是目前最快的选择。

主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
## fillTdays是根据一个股票的财务数据数据框,把月频的数据对齐到交易日tdays
fillTdays=function(data,tdays,stock.include=T) {
## data列:stock mydate StockIssuing_date,类型是chr chr Date, 后面是各列财报数据,类型为num
## tdays: chr
## stock.include表示输入是否含有stock列
## out类型:stock,mydate,各列
data=as.data.frame(data)
if(class(data$StockIssuing_date)!='Date') data$StockIssuing_date=as.Date(as.character(data$StockIssuing_date))
out=data.frame(matrix(NaN,ncol = ncol(data)-1,nrow = length(tdays)))
if(stock.include) {
names(out)=names(data)[-3]; # 去掉StockIssuing
out$stock=data$stock[1]
} else {
names(out)=names(data)[-2] # 去掉StockIssuing,首列不是stock时,StockIssuing在第二列
}
if(class(tdays)!='Date') out$mydate=as.Date(tdays)
targetPoint=which(!is.na(data$StockIssuing_date))
for(p in targetPoint) {
value=data[p,(3+stock.include):ncol(data)]
out[out$mydate>=data$StockIssuing_date[p],(2+stock.include):ncol(out)]=value
}
#out[is.na(out)]=NA
# 这里应该是NA 还是NaN ,一个是integer,一个是double类型
return(out)
}
stament=as.data.table(stament)
setkey(stament,stock,mydate)
test2=stament[,(fillTdays(.SD,tdays,stock.include = F)),by='stock']

从上面最后一句可以看出来,stament是data.table数据类型,列的位置上(fillTdays(.SD,tdays,stock.include = F))是最关键的用法,fillTdays是自定义的函数,返回的是一个数据框,.SD是stament里除去stock列的剩余数据(因为by=stock作为了分组变量),你可以将.SD理解为某一stock对应的数据框,不含stock列。

重要提示来了,在写函数时,要注意输入参数data是从.SD传递过来的,因此它也data.table格式,这个时候我们要转换成data.frame格式才可以在函数内部随心所欲地做自己喜欢做的事,返回值的类型也应该是data.frame类型,但这个并不影响最终的结果类型(依然是data.table)。

另一个重要提示是,如果函数返回的是一列数而非一个多列的数据框,那么应该这样写:

1
test2=stament[,.(mydate,fillTdays(.SD,tdays,stock.include = F)),by='stock']

这是因为.( )表示数据框列表,.(mydate,fillTdays(.SD,tdays,stock.include = F))恰好就是一个数据框列表,内含两列,而上面的stament[,(fillTdays(.SD,tdays,stock.include = F)),by='stock']没有使用.( )是因为函数的返回值就已经是数据框的类型了。

目前data.table还有一个超快的用法就是排序,这跟用R原生的order比较时就可以感受出来了。

1
setkey(stament,stock,mydate)

这条语句,不仅把我的财务数据先按stock排序,再按日期mydate排序,最重要的是设置了主键,这在merge里面充分震撼了我。1900万的两个数据merge只需要几秒。果然data.table很值得研究各种有意思的玩法。